/****************************************************** 
 *  Copyright 2018 IBM Corporation 
 *  Licensed under the Apache License, Version 2.0 (the "License"); 
 *  you may not use this file except in compliance with the License. 
 *  You may obtain a copy of the License at 
 *  http://www.apache.org/licenses/LICENSE-2.0 
 *  Unless required by applicable law or agreed to in writing, software 
 *  distributed under the License is distributed on an "AS IS" BASIS, 
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 *  See the License for the specific language governing permissions and 
 *  limitations under the License.
 */

package com.zhaoziqian.client;

import org.hyperledger.fabric.protos.peer.Query;
import org.hyperledger.fabric.sdk.BlockEvent.TransactionEvent;
import org.hyperledger.fabric.sdk.*;
import org.hyperledger.fabric.sdk.TransactionRequest.Type;
import org.hyperledger.fabric.sdk.exception.ChaincodeEndorsementPolicyParseException;
import org.hyperledger.fabric.sdk.exception.InvalidArgumentException;
import org.hyperledger.fabric.sdk.exception.ProposalException;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.logging.Level;
import java.util.logging.Logger;

import static java.nio.charset.StandardCharsets.UTF_8;

/**
 * Created with fabric-cli
 * 作者： zhaoziqian
 * 时间： 2018/5/28 下午8:35
 * 功能：
 */
public class ChannelClient {

    private org.slf4j.Logger logger = LoggerFactory.getLogger(ChannelClient.class);

    String name;
    Channel channel;
    FabricClient fabClient;

    public String getName() {
        return name;
    }

    public Channel getChannel() {
        return channel;
    }

    public FabricClient getFabClient() {
        return fabClient;
    }

    /**
     * Constructor
     *
     * @param name
     * @param channel
     * @param fabClient
     */
    public ChannelClient(String name, Channel channel, FabricClient fabClient) {
        this.name = name;
        this.channel = channel;
        this.fabClient = fabClient;
    }

    /**
     * Query by chaincode.
     *
     * @param chaincodeName
     * @param functionName
     * @param args
     * @return
     * @throws InvalidArgumentException
     * @throws ProposalException
     */
    public Collection<ProposalResponse> queryByChainCode(String chaincodeName, String functionName, String[] args)
            throws InvalidArgumentException, ProposalException {
//        Logger.getLogger(ChannelClient.class.getName()).log(Level.INFO,
//                "Querying " + functionName + " on channel " + channel.getName());
        logger.info("Querying {} on channel {} .", functionName, channel.getName());
        QueryByChaincodeRequest request = fabClient.getInstance().newQueryProposalRequest();
        ChaincodeID ccid = ChaincodeID.newBuilder().setName(chaincodeName).build();
        request.setChaincodeID(ccid);
        request.setFcn(functionName);
        if (args != null)
            request.setArgs(args);

        Collection<ProposalResponse> response = channel.queryByChaincode(request);

        return response;
    }

    /**
     * Send transaction proposal.
     *
     * @param request
     * @return
     * @throws ProposalException
     * @throws InvalidArgumentException
     */
    public Collection<ProposalResponse> sendTransactionProposal(TransactionProposalRequest request)
            throws ProposalException, InvalidArgumentException {
//        Logger.getLogger(ChannelClient.class.getName()).log(Level.INFO,
//                "Sending transaction proposal on channel " + channel.getName());
        logger.info("Sending transaction proposal on channel {} .", channel.getName());

        Collection<ProposalResponse> response = channel.sendTransactionProposal(request, channel.getPeers());
        for (ProposalResponse pres : response) {
            String stringResponse = new String(pres.getChaincodeActionResponsePayload());
            Logger.getLogger(ChannelClient.class.getName()).log(Level.INFO,
                    "Transaction proposal on channel " + channel.getName() + " " + pres.getMessage() + " "
                            + pres.getStatus() + " with transaction id:" + pres.getTransactionID());
            Logger.getLogger(ChannelClient.class.getName()).log(Level.INFO, stringResponse);
        }

        CompletableFuture<TransactionEvent> cf = channel.sendTransaction(response);
        Logger.getLogger(ChannelClient.class.getName()).log(Level.INFO, cf.toString());

        return response;
    }

    /**
     * Instantiate chaincode.
     *
     * @param chaincodeName
     * @param version
     * @param chaincodePath
     * @param language
     * @param functionName
     * @param functionArgs
     * @param policyPath    背书策略有两个主要组成部分：
     *                      1. 主体principal
     *                      2. 阀门threshold gate
     *                      <p>
     *                      P 标识期望背书的区块链节点
     *                      T 有两个输入参数：整数t（背书数量）和n （背书节点列表），即满足t的条件，并得到n的背书。
     *                      <p>
     *                      例如:
     *                      T(2, 'A', 'B', 'C') 请求来自'A'、'B'、'C'的任意2个背书节点的签名
     *                      T(1, 'A', T(2, 'B', 'C')) 请求来自A或来自B和C中的一个签名
     *                      <p>
     *                      命令行下的背书策略语法:
     *                      <p>
     *                      在Fabric CLI中，使用了一种简单的boolean表达式来解释Endorse节点的背书策略。
     *                      <p>
     *                      Fabric 1.0使用MSP（成员管理服务）来描述主体principal，该MSP用于验证签名者的身份以及签名者在该MSP内所具有的权限。
     *                      目前，支持两种角色：成员和管理员。 主体Principals的通用表现形式是MSP.ROLE，其中MSP是指MSP 的ID，ROLE是 member
     *                      或admin。 一个有效主体的示例是“Org0.admin”（Org0 MSP的任意管理员）或“Org1.member”（Org1 MSP的任意成员）。
     *                      <p>
     *                      命令行语法是这样的：
     *                      <p>
     *                      EXPR(E[, E...])
     *                      其中EXPR可以是AND或OR，代表两个boolean表达式，E是主体或对EXPR的另一个嵌套调用。
     *                      <p>
     *                      例如：
     *                      AND('Org1.member', 'Org2.member', 'Org3.member')      请求三个背书节点的签名
     *                      OR('Org1.member', 'Org2.member')                      请求两个背书节点中的任意一个的签名
     *                      OR('Org1.member', AND('Org2.member', 'Org3.member'))  请求来自Org1 MSP成员或来自Org2 MSP成员和来自Org3 MSP成员的任意一个签名
     * @return
     * @throws InvalidArgumentException
     * @throws ProposalException
     * @throws ChaincodeEndorsementPolicyParseException
     * @throws IOException
     */
    public Collection<ProposalResponse> instantiateChainCode(String chaincodeName, String version, String chaincodePath,
                                                             String language, String functionName, String[] functionArgs, String policyPath)
            throws InvalidArgumentException, ProposalException, ChaincodeEndorsementPolicyParseException, IOException {
//        Logger.getLogger(ChannelClient.class.getName()).log(Level.INFO,
//                "Instantiate proposal request " + chaincodeName + " on channel " + channel.getName()
//                        + " with Fabric client " + fabClient.getInstance().getUserContext().getMspId() + " "
//                        + fabClient.getInstance().getUserContext().getName());
        logger.info("Instantiate proposal request {} on channle {} with Fabric client {} {}",
                chaincodeName, channel.getName(),
                fabClient.getInstance().getUserContext().getMspId(),
                fabClient.getInstance().getUserContext().getName());

        InstantiateProposalRequest instantiateProposalRequest = fabClient.getInstance()
                .newInstantiationProposalRequest();
        instantiateProposalRequest.setProposalWaitTime(180000);
        ChaincodeID.Builder chaincodeIDBuilder = ChaincodeID.newBuilder().setName(chaincodeName).setVersion(version)
                .setPath(chaincodePath);
        ChaincodeID ccid = chaincodeIDBuilder.build();
        Logger.getLogger(ChannelClient.class.getName()).log(Level.INFO,
                "Instantiating Chaincode ID " + chaincodeName + " on channel " + channel.getName());

        instantiateProposalRequest.setChaincodeID(ccid);
        if (language.equals(Type.GO_LANG.toString()))
            instantiateProposalRequest.setChaincodeLanguage(Type.GO_LANG);
        else
            instantiateProposalRequest.setChaincodeLanguage(Type.JAVA);

        instantiateProposalRequest.setFcn(functionName);
        instantiateProposalRequest.setArgs(functionArgs);
        Map<String, byte[]> tm = new HashMap<>();
        tm.put("HyperLedgerFabric", "InstantiateProposalRequest:JavaSDK".getBytes(UTF_8));
        tm.put("method", "InstantiateProposalRequest".getBytes(UTF_8));
        instantiateProposalRequest.setTransientMap(tm);

        if (policyPath != null) {
            ChaincodeEndorsementPolicy chaincodeEndorsementPolicy = new ChaincodeEndorsementPolicy();
            chaincodeEndorsementPolicy.fromYamlFile(new File(policyPath));
            instantiateProposalRequest.setChaincodeEndorsementPolicy(chaincodeEndorsementPolicy);
        }

        Collection<ProposalResponse> responses = channel.sendInstantiationProposal(instantiateProposalRequest);
        CompletableFuture<TransactionEvent> cf = channel.sendTransaction(responses);

        Logger.getLogger(ChannelClient.class.getName()).log(Level.INFO,
                "Chaincode " + chaincodeName + " on channel " + channel.getName() + " instantiation " + cf);
        return responses;
    }

    /**
     * Query a transaction by id.
     *
     * @param txnId
     * @return
     * @throws ProposalException
     * @throws InvalidArgumentException
     */
    public TransactionInfo queryByTransactionId(String txnId) throws ProposalException, InvalidArgumentException {
        Logger.getLogger(ChannelClient.class.getName()).log(Level.INFO,
                "Querying by trasaction id " + txnId + " on channel " + channel.getName());
        Collection<Peer> peers = channel.getPeers();
        for (Peer peer : peers) {
            TransactionInfo info = channel.queryTransactionByID(peer, txnId);
            return info;
        }
        return null;
    }

    /**
     * Query all joined peer in the channel
     *
     * @return
     */
    public Collection<Peer> queryPeers() {
        logger.info("Querying all peers in channel {}", channel.getName());
        return channel.getPeers();
    }

    public BlockInfo queryBlockByNumber(long blockNumber) throws ProposalException, InvalidArgumentException {
        return channel.queryBlockByNumber(blockNumber);
    }

    public BlockInfo queryBlockByNumber(Peer peer, long blockNumber) throws ProposalException, InvalidArgumentException {
        BlockInfo responseBlock = null;
        responseBlock = channel.queryBlockByNumber(peer, blockNumber);
        return responseBlock;
    }

    public TransactionInfo queryTransactionByID(String txID) throws InvalidArgumentException, ProposalException {
        return channel.queryTransactionByID(txID);
    }

    public TransactionInfo queryTransactionByID(Peer peer, String txID) throws InvalidArgumentException, ProposalException {
        return channel.queryTransactionByID(peer, txID);
    }

    public BlockchainInfo queryBlockchainInfo() throws InvalidArgumentException, ProposalException {
        return channel.queryBlockchainInfo();
    }

    public BlockchainInfo queryBlockchainInfo(Peer peer) throws InvalidArgumentException, ProposalException {
        return channel.queryBlockchainInfo(peer);
    }

    public List<Query.ChaincodeInfo> queryInstalledChaincodes(Peer peer) throws ProposalException, InvalidArgumentException {
        return channel.queryInstalledChaincodes(peer);
    }

    public List<Query.ChaincodeInfo> queryInstantiatedChaincodes(Peer peer) throws ProposalException, InvalidArgumentException {
        List<Query.ChaincodeInfo> list = channel.queryInstantiatedChaincodes(peer);
        return list;
    }

    public Set<String> queryChannels(Peer peer) throws ProposalException, InvalidArgumentException {
        return channel.queryChannels(peer);
    }

    public Collection<Peer> getPeers() {
        return channel.getPeers();
    }
}
